/*
 * Copyright (c) 2024 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { ArkAssignStmt, ArkClass, ArkField, ArkInstanceFieldRef, ArkInstanceInvokeExpr, ArkInvokeStmt, ArkMethod, FieldSignature, FunctionType, Local, Signature, Stmt, Value } from 'arkanalyzer';
import { BaseChecker, BaseMetaData } from '../BaseChecker';
import { CheckerUtils, ClassMatcher, MatcherCallback, MatcherTypes, Rule } from '../../Index';
import Logger, { LOG_MODULE_TYPE } from 'arkanalyzer/lib/utils/logger';
import { Defects, IssueReport } from '../../model/Defects';
import { UndefinedConstant } from 'arkanalyzer/lib/core/base/Constant';

const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'AvoidMemoryLeakInAnimator');
const ReleaseMethods: string[] = ['aboutToDisappear', 'onPageHide', 'aboutToAppear', 'build'];
const EmptyMethods: string[] = ['aboutToDisappear', 'onPageHide', 'aboutToAppear'];
const Signatures: string[] = [
    '@ohosSdk/api/@ohos.animator.d.ts: AnimatorResult.finish()',
    '@ohosSdk/api/@ohos.animator.d.ts: AnimatorResult.cancel()',
];
const gMetaData: BaseMetaData = {
    severity: 3,
    ruleDocPath: 'docs/avoid-memory-leak-in-animator.md',
    description: 'First finish/cancel, then empty it.'
};

interface AnimatorsInfo {
    field: ArkField;
    sign: FieldSignature;
    releaseStmt: Stmt | null;
    emptyStmt: Stmt | null;
}

export class AvoidMemoryLeakInAnimator implements BaseChecker {
    readonly metaData: BaseMetaData = gMetaData;
    public rule: Rule;
    public issues: IssueReport[] = [];

    private clsMatcher: ClassMatcher = {
        matcherType: MatcherTypes.CLASS,
        hasViewTree: true
    };

    public registerMatchers(): MatcherCallback[] {
        const matchClassCb: MatcherCallback = {
            matcher: this.clsMatcher,
            callback: this.check
        };
        return [matchClassCb];
    }

    public check = (target: ArkClass): void => {
        const animators: AnimatorsInfo[] = [];
        const releaseMethods: ArkMethod[] = [];
        const emptyMethods: ArkMethod[] = [];
        this.getDeclaringMethods(target, releaseMethods, emptyMethods);
        for (let method of releaseMethods) {
            this.getFinishOrCancelStmt(method, animators);
        }
        const newAnimators = this.uniqueByKey(animators);
        for (let animator of newAnimators) {
            for (let method of emptyMethods) {
                this.getEmptyStmt(method, animator);
            }
            if (animator.emptyStmt === null) {
                this.reportIssue(animator.field);
            }
        }
    };

    private getDeclaringMethods(target: ArkClass, releaseMethods: ArkMethod[], emptyMethods: ArkMethod[]): void {
        const methods = target.getMethods();
        for (let method of methods) {
            const methodName = method.getSignature().getMethodSubSignature().getMethodName();
            if (ReleaseMethods.includes(methodName)) {
                releaseMethods.push(method);
            }
            if (EmptyMethods.includes(methodName)) {
                emptyMethods.push(method);
            }
        }
    }

    private getFinishOrCancelStmt(mtd: ArkMethod, animators: AnimatorsInfo[]): void {
        const stmts = mtd.getBody()?.getCfg().getStmts() ?? [];
        for (let stmt of stmts) {
            let invokeExpr = CheckerUtils.getInvokeExprFromStmt(stmt);
            if (!invokeExpr) {
                continue;
            }
            if (!(invokeExpr instanceof ArkInstanceInvokeExpr)) {
                continue;
            }
            const methodSignature = invokeExpr.getMethodSignature();
            if (Signatures.includes(methodSignature.toString())) {
                const base = invokeExpr.getBase();
                if (!(base instanceof Local)) {
                    continue;
                }
                const declaringStmt = base.getDeclaringStmt();
                if (!(declaringStmt instanceof ArkAssignStmt)) {
                    continue;
                }
                const rightOp = declaringStmt.getRightOp();
                if (!(rightOp instanceof ArkInstanceFieldRef)) {
                    continue;
                }
                const fieldSignature = rightOp.getFieldSignature();
                const field = mtd.getDeclaringArkClass().getField(fieldSignature);
                if (field === null) {
                    continue;
                }
                animators.push({ field: field, sign: fieldSignature, releaseStmt: stmt, emptyStmt: null });
            } else {
                const args = invokeExpr.getArgs();
                if (args.length !== 0) {
                    this.getFinsishOrCancelStmtByArgs(mtd, animators, args);
                }
                const method = mtd.getDeclaringArkFile().getScene().getMethod(methodSignature);
                if (method !== null) {
                    this.getFinishOrCancelStmt(method, animators);
                }
            }
        }
    }

    private getFinsishOrCancelStmtByArgs(mtd: ArkMethod, animators: AnimatorsInfo[], args: Value[]): void {
        for (let arg of args) {
            if (!(arg instanceof Local)) {
                continue;
            }
            const type = arg.getType();
            if (!(type instanceof FunctionType)) {
                continue;
            }
            const method = mtd.getDeclaringArkFile().getScene().getMethod(type.getMethodSignature());
            if (method === null) {
                continue;
            }
            this.getFinishOrCancelStmt(method, animators);
        }
    }

    private uniqueByKey(animators: AnimatorsInfo[]): AnimatorsInfo[] {
        const seen = new Set();
        return animators.filter(animator => {
            const value = animator.field;
            return !seen.has(value) && seen.add(value);
        });
    }

    private getEmptyStmt(mtd: ArkMethod, animator: AnimatorsInfo): void {
        const stmts = mtd.getBody()?.getCfg().getStmts() ?? [];
        for (let stmt of stmts) {
            if (stmt instanceof ArkInvokeStmt) {
                const invokeExpr = stmt.getInvokeExpr();
                if (!(invokeExpr instanceof ArkInstanceInvokeExpr)) {
                    continue;
                }
                const methodSignature = invokeExpr.getMethodSignature();
                const arkFile = stmt.getCfg().getDeclaringMethod().getDeclaringArkFile();
                const declaringMethod = arkFile.getScene().getMethod(methodSignature);
                if (declaringMethod === null) {
                    continue;
                }
                this.getEmptyStmt(declaringMethod, animator);
            } else if (stmt instanceof ArkAssignStmt) {
                const rightOp = stmt.getRightOp();
                if (!(rightOp instanceof UndefinedConstant)) {
                    continue;
                }
                const leftOp = stmt.getLeftOp();
                if (!(leftOp instanceof ArkInstanceFieldRef)) {
                    continue;
                }
                const fieldSignature = leftOp.getFieldSignature().toString();
                if (fieldSignature !== animator.sign.toString()) {
                    continue;
                }
                animator.emptyStmt = stmt;
                return;
            }
        }
    }

    private reportIssue(field: ArkField): void {
        const lineCode = field.getCode();
        const fieldName = field.getName();
        const originalPosition = field.getOriginPosition();
        const lineNum = originalPosition.getLineNo();
        const startColum = originalPosition.getColNo() + lineCode.indexOf(fieldName);
        const endColum = startColum + fieldName.length - 1;
        const severity = this.rule.alert ?? this.metaData.severity;
        const filePath = field.getDeclaringArkClass().getDeclaringArkFile().getFilePath();
        let defects = new Defects(lineNum, startColum, endColum, this.metaData.description, severity, this.rule.ruleId, filePath,
            this.metaData.ruleDocPath, true, false, false);
        this.issues.push(new IssueReport(defects, undefined));
    }
}